package simonxianyu.boot.mybatis;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.builder.xml.XMLStatementBuilder;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import simonxianyu.boot.mybatis.base.BatisConditionIgnore;
import simonxianyu.boot.mybatis.base.BatisMapper;
import simonxianyu.boot.mybatis.base.DbColumn;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * 向mapper注入自动生成sql的类
 */
public class MapperHelper {
  private static final Logger log = LoggerFactory.getLogger(MyMapperFactoryBean.class);
  private final Class<?> mapperClass;
  private final Configuration configuration;


  private String columns;
  private String columnsWithoutId;
  private String setColumns;
  private String placeHolder;
  private String tableName;
  private List<ResultMap> defaultResultMaps;
  private String resultMapId;
  private boolean useGeneratedKey;
  private boolean useColumnAnnotation;
  private String idName;
  private String idColumn;
  private boolean useVersion;
  private String versionColumn;
  private String versionName;

  private Class<?> modelClass;
  private String mapperName;

  public MapperHelper(Class<?> mapperClass, Configuration configuration) {
    this.mapperClass = mapperClass;
    this.configuration = configuration;
  }

  void injectMapper() {
    preCheckDao();
    buildSqls(configuration);
  }

  void injectMapper(MyMapperFactoryBean myMapperFactoryBean) {
    preCheckDao();
    myMapperFactoryBean.doCheckDaoConfig();
    buildSqls(configuration);
  }

  private void preCheckDao() {
    BatisMapper mapperAnno = mapperClass.getAnnotation(BatisMapper.class);
    if (null != mapperAnno) {
      this.useGeneratedKey = mapperAnno.useGeneratedKey();
      this.idName = mapperAnno.idName();
      this.idColumn = mapperAnno.idColumn();
      this.useVersion = mapperAnno.useVersion();
      this.versionColumn = mapperAnno.versionColumn();
      this.versionName = mapperAnno.versionName();
      this.useColumnAnnotation = mapperAnno.useColumnAnnotation();
    } else {
      this.useGeneratedKey = false;
      this.idName = "id";
      this.idColumn = "id";
      this.useVersion = false;
      this.versionColumn = "version";
      this.versionName = "version";
      this.useColumnAnnotation = true;
    }

    this.mapperName = mapperClass.getName();
//      System.out.println(mapperName);
    Type genericType1 = mapperClass.getGenericInterfaces()[0];
    if (genericType1 instanceof ParameterizedType) {
      ParameterizedType type = (ParameterizedType) genericType1;
      this.modelClass = (Class) type.getActualTypeArguments()[0];
    } else {
      modelClass = (Class) genericType1;
    }
    tableName = camelToUnder(modelClass.getSimpleName());
    buildSqlFragment(configuration, mapperName);
  }

  private void buildSqls(Configuration configuration) {

    addSelect(configuration, mapperName+".findAll",
        "SELECT " + columns + " FROM " + tableName , false);
    addSelect(configuration, mapperName + ".findAllPaged",
        "SELECT " + columns + " FROM " + tableName, false);


    String idInsert = mapperName + ".saveNew";
    if (!configuration.hasStatement(idInsert, false)) {
      String sql = "INSERT INTO " + tableName + '(' +
          (useGeneratedKey?columnsWithoutId:columns) + ") VALUES ( "
          + (useGeneratedKey || StringUtils.isBlank(idName) ?"":"#{"+idName+"},")
          + placeHolder + " )";
      log.trace("sql: {}", sql);
      SqlSource sqlSource = configuration.getLanguageRegistry().getDefaultDriver()
          .createSqlSource(configuration, sql, null);
      MappedStatement.Builder builder = new MappedStatement.Builder(configuration, idInsert,
          sqlSource, SqlCommandType.INSERT)
          .statementType(StatementType.PREPARED)
          .keyGenerator(useGeneratedKey? Jdbc3KeyGenerator.INSTANCE:NoKeyGenerator.INSTANCE)
//          .keyProperty(idName)
          .flushCacheRequired(true)
          .useCache(false)
//        .resultMaps(defaultResultMaps)
          ;
      if (useGeneratedKey && StringUtils.isNotBlank(idName)) {
        builder.keyProperty(idName);
      }
      configuration.addMappedStatement(builder.build());
    }

    if (StringUtils.isNotBlank(idName)) {
      addSelect(configuration, mapperName + ".get",
          "SELECT " + columns + " FROM " + tableName + " WHERE " + idColumn + "=#{" + idName + "}", false);
    }


    if (!configuration.hasStatement(mapperName + ".update", false)
        && StringUtils.isNotBlank(idName)
        ) {
      String sql = "UPDATE " + tableName + " SET " +setColumns;
      if (useVersion) {
        sql += " , "+versionColumn +"="+versionColumn+"+1 ";
      }
      sql += " WHERE "+idColumn+"=#{"+idName+"}";
      if (useVersion) {
        sql += " AND version=#{"+versionName+"}";
      }

      log.trace("sql: {}", sql);
      SqlSource sqlSource = configuration.getLanguageRegistry().getDefaultDriver()
          .createSqlSource(configuration, sql, null);
      MappedStatement.Builder builder = new MappedStatement.Builder(configuration, mapperName + ".update",
          sqlSource, SqlCommandType.UPDATE)
          .statementType(StatementType.PREPARED)
          .keyGenerator(NoKeyGenerator.INSTANCE)
          .flushCacheRequired(true)
          .useCache(false)
          ;
      configuration.addMappedStatement(builder.build());
    }

    if (!configuration.hasStatement(mapperName + ".deleteById", false)
        && StringUtils.isNotBlank(idName)
        ) {
      String sql = "DELETE FROM " + tableName + " WHERE "+idColumn+"=#{"+idName+"}";
      log.trace("sql: {}", sql);
      SqlSource sqlSource = configuration.getLanguageRegistry().getDefaultDriver()
          .createSqlSource(configuration, sql, null);
      MappedStatement.Builder builder = new MappedStatement.Builder(configuration, mapperName + ".deleteById",
          sqlSource, SqlCommandType.DELETE)
          .statementType(StatementType.PREPARED)
          .keyGenerator(NoKeyGenerator.INSTANCE)
          .resultMaps(defaultResultMaps)
          .flushCacheRequired(true)
          .useCache(false)
          ;
      configuration.addMappedStatement(builder.build());
    }

    if (!configuration.hasStatement(mapperName + ".count", false)) {
      String sql = "SELECT COUNT(*) FROM " + tableName ;
      log.trace("sql: {}", sql);
      SqlSource sqlSource = configuration.getLanguageRegistry().getDefaultDriver()
          .createSqlSource(configuration, sql, null);
      ResultMap resultMap = new ResultMap.Builder(configuration, mapperName+".count-Inline",
          Long.class, Collections.emptyList()).build();
      List<ResultMap> resultMaps = new ArrayList<>();
      resultMaps.add(resultMap);
      MappedStatement.Builder builder = new MappedStatement.Builder(configuration, mapperName + ".count",
          sqlSource, SqlCommandType.SELECT)
          .statementType(StatementType.PREPARED)
          .keyGenerator(NoKeyGenerator.INSTANCE)
          .resultMaps(resultMaps)
          .flushCacheRequired(false)
          .useCache(true)
          ;
      configuration.addMappedStatement(builder.build());
    }

    addSelect(configuration, mapperName + ".countByExample",
        "<select id=\""+mapperName+".countByExample"+"\" resultType=\"java.lang.Long\">\n" +
        "    SELECT COUNT(*)  FROM "+tableName+"\n" +
        "    <include refid=\"dynamicWhere\" />\n" +
        "  </select>",true);
    addSelect(configuration, mapperName + ".findByExample",  "<select id=\""+mapperName+".findByExample\" resultMap=\""+resultMapId+"\">\n" +
        "    SELECT <include refid=\"cols\" />\n" +
        "    FROM "+tableName+"\n" +
        "    <include refid=\"dynamicWhere\" />\n" +
        "  </select>", true);

  }

  private void addSelect(Configuration configuration, String statementId, String sql, boolean xml) {
    if (!configuration.hasStatement(statementId, false)) {
      log.trace("build statement with sql: {}", sql);
      String txt = xml ? sql : "<select id=\""+statementId+"\" resultMap=\""+resultMapId+"\">"+sql+"</select>";

      XPathParser pathParser = new XPathParser(txt);
      MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, mapperName+".xml");
      assistant.setCurrentNamespace(mapperName);
      XMLStatementBuilder xmlStatementBuilder = new XMLStatementBuilder(configuration, assistant,
          pathParser.evalNode("/select"), configuration.getDatabaseId()
      );
      xmlStatementBuilder.parseStatementNode();
    }
  }

  private String camelToUnder(String name) {
    return StringUtils.join(
        StringUtils.splitByCharacterTypeCamelCase(name), "_").toLowerCase();
  }

  private void buildSqlFragment(Configuration configuration, String mapperName) {
    Field[] fields = FieldUtils.getAllFields(modelClass);
    StringBuilder strColumns = new StringBuilder();
    StringBuilder strPlaceHolder = new StringBuilder(30);
    StringBuilder strColumnsWithoutId = new StringBuilder();
    StringBuilder strSetColumnsWithoutId = new StringBuilder();
    List<ResultMapping> mappings = new ArrayList<>();
    // iterate fields
    for(Field f : fields) {
      if (strColumns.length()>0) {
        strColumns.append(',');
      }
      String dbColumnName = null;
      if (useColumnAnnotation) {
        DbColumn colAnno = AnnotationUtils.getAnnotation(f, DbColumn.class);
        if (null != colAnno && StringUtils.isNotEmpty(colAnno.name())) {
          dbColumnName = colAnno.name();
        }
      }
      if (dbColumnName == null || dbColumnName.length() == 0) {
        dbColumnName = camelToUnder(f.getName());
      }
      strColumns.append('`').append(dbColumnName).append('`');

      if (strColumnsWithoutId.length()>0) {
        strColumnsWithoutId.append(',');
        strPlaceHolder.append(',');
        strSetColumnsWithoutId.append(',');
      }
      if (!idName.equals(f.getName())) {
        strColumnsWithoutId.append('`').append(dbColumnName).append('`');
        strPlaceHolder.append("#{").append(f.getName()).append('}');
        strSetColumnsWithoutId.append('`').append(dbColumnName).append('`')
            .append("=#{").append(f.getName()).append('}');
      }
      ResultMapping mapping = new ResultMapping.Builder(configuration, f.getName(),
          dbColumnName, f.getType()
      ).build();
      mappings.add(mapping);
    }
    columns = strColumns.toString();
    placeHolder = strPlaceHolder.toString();
    columnsWithoutId = strColumnsWithoutId.toString();
    setColumns = strSetColumnsWithoutId.toString();

    Map<String, XNode> sqlFragments = configuration.getSqlFragments();
    String sqlId = mapperName + ".cols";
    if (!sqlFragments.containsKey(sqlId)) {
      String sql = "<sql id=\"" + sqlId + "\">" + columns + "</sql>";
      log.debug("generate: {}", sql);
      XPathParser parser = new XPathParser(sql);
      sqlFragments.put(sqlId, parser.evalNode("/sql"));
    }

    sqlId = mapperName + ".valCols";
    if (!sqlFragments.containsKey(sqlId)) {
      XPathParser parser = new XPathParser("<sql id=\""+sqlId+"\">"+columnsWithoutId+"</sql>");
      sqlFragments.put(sqlId, parser.evalNode("/sql"));
    }

    sqlId = mapperName + ".dynamicWhere";
    if (!sqlFragments.containsKey(sqlId)) {
      StringBuilder sqlBuilder = new StringBuilder("<sql id=\"" + sqlId + "\"><where>\n");
      for(Field f : fields) {
        String uname = camelToUnder(f.getName());
        String fname = f.getName();
        if (f.getType().isPrimitive()) {
          continue;
        }
        BatisConditionIgnore ignoreAnnotate = f.getAnnotation(BatisConditionIgnore.class);
        if (ignoreAnnotate != null) {
          continue;
        }
        if (String.class.equals(f.getType())) {
          sqlBuilder.append("<if test=\"").append(fname).append(" !='' and ").append(fname)
              .append(" != null\">AND `").append(uname)
              .append("` = #{").append(fname).append("}</if>\n");
        } else {
          sqlBuilder.append("<if test=\"").append(fname)
              .append(" != null\">AND `").append(uname)
              .append("` = #{").append(fname).append("}</if>\n");
        }
      }
      sqlBuilder.append("</where></sql>");
      String sql = sqlBuilder.toString();
      log.trace("sql: {}", sql);
      XPathParser parser = new XPathParser(sql);
      sqlFragments.put(sqlId, parser.evalNode("/sql"));
    }

    String simpleName = modelClass.getSimpleName();
    simpleName = Character.toLowerCase(simpleName.charAt(0))+simpleName.substring(1);
    resultMapId = mapperName+"."+ simpleName + "Result";

    if (!configuration.hasResultMap(resultMapId)) {
      log.debug("register resultMap {}", resultMapId);
      ResultMap.Builder builder = new ResultMap.Builder(configuration, resultMapId, modelClass,
          mappings);
      ResultMap defaultResultMap = builder.build();
      defaultResultMaps = new ArrayList<>();
      defaultResultMaps.add(defaultResultMap);
      configuration.addResultMap(defaultResultMap);
    }

  }
}
